<?php

declare(strict_types=1);

/*
 * This file is part of PHP CS Fixer.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PhpCsFixer\Tests\Fixer\ConstantNotation;

use PhpCsFixer\ConfigurationException\InvalidFixerConfigurationException;
use PhpCsFixer\Preg;
use PhpCsFixer\Tests\Test\AbstractFixerTestCase;

/**
 * @internal
 *
 * @covers \PhpCsFixer\Fixer\ConstantNotation\NativeConstantInvocationFixer
 *
 * @extends AbstractFixerTestCase<\PhpCsFixer\Fixer\ConstantNotation\NativeConstantInvocationFixer>
 *
 * @author Filippo Tessarotto <zoeslam@gmail.com>
 *
 * @phpstan-import-type _AutogeneratedInputConfiguration from \PhpCsFixer\Fixer\ConstantNotation\NativeConstantInvocationFixer
 *
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
 */
final class NativeConstantInvocationFixerTest extends AbstractFixerTestCase
{
    /**
     * @param array<string, mixed> $configuration
     *
     * @dataProvider provideInvalidConfigurationCases
     */
    public function testInvalidConfiguration(array $configuration, string $exceptionExpression): void
    {
        $this->expectException(InvalidFixerConfigurationException::class);
        $this->expectExceptionMessage($exceptionExpression);

        $this->fixer->configure($configuration);
    }

    /**
     * @return iterable<string, array{array<string, mixed>, string}>
     */
    public static function provideInvalidConfigurationCases(): iterable
    {
        yield 'unknown configuration key' => [
            ['foo' => 'bar'],
            '[native_constant_invocation] Invalid configuration: The option "foo" does not exist.',
        ];

        yield 'invalid exclude configuration element - null' => [
            ['include' => [null]],
            '[native_constant_invocation] Invalid configuration: The option "include" with value array is expected to be of type "string[]", but one of the elements is of type "null".',
        ];

        yield 'invalid exclude configuration element - false' => [
            ['include' => [false]],
            '[native_constant_invocation] Invalid configuration: The option "include" with value array is expected to be of type "string[]", but one of the elements is of type "bool".',
        ];

        yield 'invalid exclude configuration element - true' => [
            ['include' => [true]],
            '[native_constant_invocation] Invalid configuration: The option "include" with value array is expected to be of type "string[]", but one of the elements is of type "bool".',
        ];

        yield 'invalid exclude configuration element - int' => [
            ['include' => [1]],
            '[native_constant_invocation] Invalid configuration: The option "include" with value array is expected to be of type "string[]", but one of the elements is of type "int".',
        ];

        yield 'invalid exclude configuration element - array' => [
            ['include' => [[]]],
            '[native_constant_invocation] Invalid configuration: The option "include" with value array is expected to be of type "string[]", but one of the elements is of type "array".',
        ];

        yield 'invalid exclude configuration element - float' => [
            ['include' => [0.1]],
            '[native_constant_invocation] Invalid configuration: The option "include" with value array is expected to be of type "string[]", but one of the elements is of type "float".',
        ];

        yield 'invalid exclude configuration element - object' => [
            ['include' => [new \stdClass()]],
            '[native_constant_invocation] Invalid configuration: The option "include" with value array is expected to be of type "string[]", but one of the elements is of type "stdClass".',
        ];

        yield 'invalid exclude configuration element - not-trimmed' => [
            ['include' => ['  M_PI  ']],
            '[native_constant_invocation] Invalid configuration: Each element must be a non-empty, trimmed string, got "string" instead.',
        ];
    }

    /**
     * @param _AutogeneratedInputConfiguration $configuration
     *
     * @dataProvider provideFixCases
     */
    public function testFix(string $expected, ?string $input = null, array $configuration = []): void
    {
        $this->fixer->configure($configuration);
        $this->doTest($expected, $input);
    }

    /**
     * @return iterable<array{0: string, 1?: null|string, 2?: _AutogeneratedInputConfiguration}>
     */
    public static function provideFixCases(): iterable
    {
        yield ['<?php var_dump(NULL, FALSE, TRUE, 1);'];

        yield ['<?php echo CUSTOM_DEFINED_CONSTANT_123;'];

        yield ['<?php echo m_pi; // Constant are case sensitive'];

        yield ['<?php namespace M_PI;'];

        yield ['<?php namespace Foo; use M_PI;'];

        yield ['<?php class M_PI {}'];

        yield ['<?php class Foo extends M_PI {}'];

        yield ['<?php class Foo implements M_PI {}'];

        yield ['<?php interface M_PI {};'];

        yield ['<?php trait M_PI {};'];

        yield ['<?php class Foo { const M_PI = 1; }'];

        yield ['<?php class Foo { use M_PI; }'];

        yield ['<?php class Foo { public $M_PI = 1; }'];

        yield ['<?php class Foo { function M_PI($M_PI) {} }'];

        yield ['<?php class Foo { function bar() { $M_PI = M_PI() + self::M_PI(); } }'];

        yield ['<?php class Foo { function bar() { $this->M_PI(self::M_PI); } }'];

        yield ['<?php namespace Foo; use Bar as M_PI;'];

        yield ['<?php echo Foo\M_PI\Bar;'];

        yield ['<?php M_PI::foo();'];

        yield ['<?php function x(M_PI $foo, M_PI &$bar, M_PI ...$baz) {}'];

        yield ['<?php $foo instanceof M_PI;'];

        yield ['<?php class x implements FOO, M_PI, BAZ {}'];

        yield ['<?php class Foo { use Bar, M_PI { Bar::baz insteadof M_PI; } }'];

        yield ['<?php M_PI: goto M_PI;'];

        yield [
            '<?php echo \M_PI;',
            '<?php echo M_PI;',
        ];

        yield [
            '<?php namespace Foo; use M_PI; echo \M_PI;',
            '<?php namespace Foo; use M_PI; echo M_PI;',
        ];

        yield [
            // Here we are just testing the algorithm.
            // A user likely would add this M_PI to its excluded list.
            '<?php namespace M_PI; const M_PI = 1; return \M_PI;',
            '<?php namespace M_PI; const M_PI = 1; return M_PI;',
        ];

        yield [
            '<?php foo(\E_DEPRECATED | \E_USER_DEPRECATED);',
            '<?php foo(E_DEPRECATED | E_USER_DEPRECATED);',
        ];

        yield ['<?php function foo(): M_PI {}'];

        yield ['<?php use X\Y\{FOO, BAR as BAR2, M_PI};'];

        yield [
            '<?php
try {
    foo(\JSON_ERROR_DEPTH|\JSON_PRETTY_PRINT|JOB_QUEUE_PRIORITY_HIGH);
} catch (\Exception | \InvalidArgumentException|\UnexpectedValueException|LogicException $e) {
}
',
            '<?php
try {
    foo(\JSON_ERROR_DEPTH|JSON_PRETTY_PRINT|\JOB_QUEUE_PRIORITY_HIGH);
} catch (\Exception | \InvalidArgumentException|\UnexpectedValueException|LogicException $e) {
}
',
        ];

        yield [
            '<?php echo \FOO_BAR_BAZ . \M_PI;',
            '<?php echo FOO_BAR_BAZ . M_PI;',
            ['include' => ['FOO_BAR_BAZ']],
        ];

        yield [
            '<?php class Foo { public function bar($foo) { return \FOO_BAR_BAZ . \M_PI; } }',
            '<?php class Foo { public function bar($foo) { return FOO_BAR_BAZ . M_PI; } }',
            ['include' => ['FOO_BAR_BAZ']],
        ];

        yield [
            '<?php echo PHP_SAPI . FOO_BAR_BAZ . \M_PI;',
            '<?php echo PHP_SAPI . FOO_BAR_BAZ . M_PI;',
            [
                'fix_built_in' => false,
                'include' => ['M_PI'],
            ],
        ];

        yield [
            '<?php class Foo { public function bar($foo) { return PHP_SAPI . FOO_BAR_BAZ . \M_PI; } }',
            '<?php class Foo { public function bar($foo) { return PHP_SAPI . FOO_BAR_BAZ . M_PI; } }',
            [
                'fix_built_in' => false,
                'include' => ['M_PI'],
            ],
        ];

        yield [
            '<?php echo \PHP_SAPI . M_PI;',
            '<?php echo PHP_SAPI . M_PI;',
            ['exclude' => ['M_PI']],
        ];

        yield [
            '<?php class Foo { public function bar($foo) { return \PHP_SAPI . M_PI; } }',
            '<?php class Foo { public function bar($foo) { return PHP_SAPI . M_PI; } }',
            ['exclude' => ['M_PI']],
        ];

        yield 'null true false are case insensitive' => [
            <<<'EOT'
                <?php
                var_dump(
                    \null,
                    \NULL,
                    \Null,
                    \nUlL,
                    \false,
                    \FALSE,
                    true,
                    TRUE,
                    \M_PI,
                    \M_pi,
                    m_pi,
                    m_PI
                );
                EOT,
            <<<'EOT'
                <?php
                var_dump(
                    null,
                    NULL,
                    Null,
                    nUlL,
                    false,
                    FALSE,
                    true,
                    TRUE,
                    M_PI,
                    M_pi,
                    m_pi,
                    m_PI
                );
                EOT,
            [
                'fix_built_in' => false,
                'include' => [
                    'null',
                    'false',
                    'M_PI',
                    'M_pi',
                ],
                'exclude' => [],
            ],
        ];
        $uniqueConstantName = uniqid(self::class);
        $uniqueConstantName = Preg::replace('/\W+/', '_', $uniqueConstantName);
        $uniqueConstantName = strtoupper($uniqueConstantName);

        $dontFixMe = 'DONTFIXME_'.$uniqueConstantName;
        $fixMe = 'FIXME_'.$uniqueConstantName;

        \define($dontFixMe, 1);
        \define($fixMe, 1);

        yield 'do not include user constants unless explicitly listed' => [
            <<<EOT
                <?php
                var_dump(
                    \\null,
                    {$dontFixMe},
                    \\{$fixMe}
                );
                EOT,
            <<<EOT
                <?php
                var_dump(
                    null,
                    {$dontFixMe},
                    {$fixMe}
                );
                EOT,
            [
                'fix_built_in' => true,
                'include' => [
                    $fixMe,
                ],
                'exclude' => [],
            ],
        ];

        yield 'do not fix imported constants' => [
            <<<'EOT'
                <?php

                namespace Foo;

                use const M_EULER;

                var_dump(
                    null,
                    \M_PI,
                    M_EULER
                );
                EOT,
            <<<'EOT'
                <?php

                namespace Foo;

                use const M_EULER;

                var_dump(
                    null,
                    M_PI,
                    M_EULER
                );
                EOT,
            [
                'fix_built_in' => false,
                'include' => [
                    'M_PI',
                    'M_EULER',
                ],
                'exclude' => [],
            ],
        ];

        yield 'scoped only' => [
            <<<'EOT'
                <?php

                namespace space1 {
                    echo \PHP_VERSION;
                }
                namespace {
                    echo PHP_VERSION;
                }
                EOT,
            <<<'EOT'
                <?php

                namespace space1 {
                    echo PHP_VERSION;
                }
                namespace {
                    echo PHP_VERSION;
                }
                EOT,
            ['scope' => 'namespaced'],
        ];

        yield 'scoped only no namespace' => [
            <<<'EOT'
                <?php

                echo PHP_VERSION . PHP_EOL;
                EOT,
            null,
            ['scope' => 'namespaced'],
        ];

        yield 'strict option' => [
            '<?php
                echo \PHP_VERSION . \PHP_EOL; // built-in constants to have backslash
                echo MY_FRAMEWORK_MAJOR_VERSION . MY_FRAMEWORK_MINOR_VERSION; // non-built-in constants not to have backslash
                echo \Dont\Touch\Namespaced\CONSTANT;
            ',
            '<?php
                echo \PHP_VERSION . PHP_EOL; // built-in constants to have backslash
                echo \MY_FRAMEWORK_MAJOR_VERSION . MY_FRAMEWORK_MINOR_VERSION; // non-built-in constants not to have backslash
                echo \Dont\Touch\Namespaced\CONSTANT;
            ',
            ['strict' => true],
        ];
    }

    /**
     * @requires PHP <8.0
     */
    public function testFixPre80(): void
    {
        $this->doTest(
            '<?php
echo \/**/M_PI;
echo \ M_PI;
echo \#
#
M_PI;
echo \M_PI;
',
            '<?php
echo \/**/M_PI;
echo \ M_PI;
echo \#
#
M_PI;
echo M_PI;
'
        );
    }

    /**
     * @dataProvider provideFix80Cases
     *
     * @requires PHP 8.0
     */
    public function testFix80(string $expected): void
    {
        $this->fixer->configure(['strict' => true]);
        $this->doTest($expected);
    }

    /**
     * @return iterable<int, array{string}>
     */
    public static function provideFix80Cases(): iterable
    {
        yield [
            '<?php
            try {
            } catch (\Exception) {
            }',
        ];

        yield ['<?php try { foo(); } catch(\InvalidArgumentException|\LogicException $e) {}'];

        yield ['<?php try { foo(); } catch(\InvalidArgumentException|\LogicException) {}'];
    }

    /**
     * @dataProvider provideFix81Cases
     *
     * @requires PHP 8.1
     */
    public function testFix81(string $expected): void
    {
        $this->fixer->configure(['strict' => true]);
        $this->doTest($expected);
    }

    /**
     * @return iterable<int, array{string}>
     */
    public static function provideFix81Cases(): iterable
    {
        yield [
            '<?php enum someEnum: int
                {
                    case E_ALL = 123;
                }',
        ];
    }

    /**
     * @dataProvider provideFix82Cases
     *
     * @requires PHP 8.2
     */
    public function testFix82(string $expected): void
    {
        $this->fixer->configure(['strict' => true]);
        $this->doTest($expected);
    }

    /**
     * @return iterable<int, array{0: string}>
     */
    public static function provideFix82Cases(): iterable
    {
        yield ['<?php class Foo { public (\A&B)|(C&\D)|E\F|\G|(A&H\I)|(A&\J\K) $var; }'];

        yield ['<?php function foo ((\A&B)|(C&\D)|E\F|\G|(A&H\I)|(A&\J\K) $var) {}'];
    }

    /**
     * @dataProvider provideFix83Cases
     *
     * @requires PHP 8.3
     */
    public function testFix83(string $expected, string $input): void
    {
        $this->fixer->configure(['strict' => true]);
        $this->doTest($expected, $input);
    }

    /**
     * @return iterable<int, array{0: string, 1: string}>
     */
    public static function provideFix83Cases(): iterable
    {
        yield [
            '<?php class Foo {
                public const string C1 = \PHP_EOL;
                protected const string|int C2 = \PHP_EOL;
                private const string|(A&B) C3 = BAR;
                public const EnumA C4 = EnumA::FOO;
                private const array CONNECTION_TIMEOUT = [\'foo\'];
            }',
            '<?php class Foo {
                public const string C1 = PHP_EOL;
                protected const string|int C2 = \PHP_EOL;
                private const string|(A&B) C3 = \BAR;
                public const EnumA C4 = EnumA::FOO;
                private const array CONNECTION_TIMEOUT = [\'foo\'];
            }',
        ];
    }
}
